from math import sqrt, hypot, sin, cos, pi

import pygame

from geometry import getDirection, pointOnLine
import primitives
import blocks
import render
import particle
from level import Level
from quadtree import QuadTree
from menu import sinInterpolation

#Dynamics constants
GRAVITY = 0.04
FRICTION = 0.003

#Gameplay constants
ROOM_WIDTH = 640
ROOM_HEIGHT = 480

STARTING_BALLS = 12
BALL_SIZE = 12
BALL_MAX_BUMPS = 64

BLOCK_SIZE = 16

PADDLE_WIDTH = 192
PADDLE_HEIGHT = 40
PADDLE_FIX = 0.1

#Graphics constants
COLOR0 = 255, 255, 255
COLOR1 = 255, 127, 0
COLOR2 = 255, 0, 0
COLOR3 = 0, 0, 0

class GUI:
    def __init__(self, x, y, color=COLOR1):
        self.x = x
        self.y = y
        self.color = color
        
        self.score = 0
        self.scoreText = "Score: "
        
        self.balls = 0
        self.ballsText = "Balls: "
        
        self.font = pygame.font.Font(None, 28)
        
        self.scoreImage = self.font.render(self.scoreText + str(self.score), True, self.color)
        self.ballsImage = self.font.render(self.ballsText + str(self.balls), True, self.color)
    
    def update(self):
        pass
    
    def drawImage(self):
        self.scoreImage = self.font.render(self.scoreText + str(self.score), True, self.color)
        self.ballsImage = self.font.render(self.ballsText + str(self.balls), True, self.color)
    
    def draw(self, display):
        display.blit(self.scoreImage, (self.x, self.y))
        display.blit(self.ballsImage, (self.x, self.y+30))

class BallWidget:
    def __init__(self, x, y, w, h):
        self.x = x
        self.y = y
        self.w = w
        self.h = h
        self.r = r = h / 2.0
        self.pad = p = 2.0
        
        self.bgCapsule = primitives.Capsule(x+r, y+r, x+w - r, y+r, r)
        self.fgCapsule = primitives.Capsule(x+r, y+r, x+w - r, y+r, r - p)
        
        self.bgImage = None
        self.fgImage = None
    
    def drawImage(self, fullness=1.0):
        if not self.bgImage:
            self.bgImage = render.silhouette(self.bgCapsule, COLOR1, True)
        if fullness == 0:
            self.fgCapsule.x1 = self.fgCapsule.x0 + 1
            self.fgImage = render.silhouette(self.fgCapsule, COLOR1, True)
        else:
            w = (self.w - self.r * 2) * fullness
            self.fgCapsule.x1 = self.fgCapsule.x0 + w
            self.fgImage = render.silhouette(self.fgCapsule, COLOR3, True)
    
    def draw(self, display):
        display.blit(self.bgImage, (self.x, self.y))
        display.blit(self.fgImage, (self.x + self.pad, self.y + self.pad))

class ScoreWidget:
    def __init__(self):
        self.x = x
        self.y = y
        self.color = color
        
        self.score = 0
        self.scoreText = "Score: "
        
        self.balls = 0
        self.ballsText = "Balls: "
        
        self.font = pygame.font.Font(None, 28)
        
        self.scoreImage = self.font.render(self.scoreText + str(self.score), True, self.color)
        self.ballsImage = self.font.render(self.ballsText + str(self.balls), True, self.color)
    
    def update(self):
        pass
    
    def drawImage(self):
        self.scoreImage = self.font.render(self.scoreText + str(self.score), True, self.color)
        self.ballsImage = self.font.render(self.ballsText + str(self.balls), True, self.color)
    
    def draw(self, display):
        display.blit(self.scoreImage, (self.x, self.y))
        display.blit(self.ballsImage, (self.x, self.y+30))

class Ball(primitives.Circle):
    def __init__(self, x, y, radius=BALL_SIZE, direction=0, speed=2.8):
        primitives.Circle.__init__(self, x, y, radius)
        
        self.xPrev = self.x - cos(direction) * speed
        self.yPrev = self.y - sin(direction) * speed
        
        self.bumps = 0
        
        self.rect = pygame.Rect(self.getRectangle())
        self.image = render.silhouette(self, COLOR3, True)
    
    def accelerate(self, multiplier):
        self.xPrev = self.xPrev - (self.x - self.xPrev) * multiplier
        self.yPrev = self.yPrev - (self.y - self.yPrev) * multiplier
    
    def update(self):
        if self.x < 0 or self.x > ROOM_WIDTH:
            self.x, self.xPrev = self.xPrev, self.x
        self.x, self.xPrev = self.x * (2 - FRICTION) - self.xPrev * (1 - FRICTION), self.x
        self.y, self.yPrev = self.y * (2 - FRICTION) - self.yPrev * (1 - FRICTION), self.y
        self.yPrev -= GRAVITY
        
        self.rect.center = self.x, self.y
    
    def draw(self, display):
        display.blit(self.image, self.rect)

class Emitter(primitives.Circle):
    def __init__(self, x, y, radius=BALL_SIZE):
        primitives.Circle.__init__(self, x, y, radius)
        self.x = x
        self.y = y
        self.radius = radius
        
        self.rect = pygame.Rect(self.getRectangle())
        self.image = render.silhouette(self, COLOR3, True)
    
    def update(self):
        pass
    
    def draw(self, display):
        display.blit(self.image, self.rect)
        x0, y0 = pygame.mouse.get_pos()
        direction = getDirection(self.x, self.y, x0, y0)
        x1 = self.x + cos(direction - 0.17) * self.radius + 1
        y1 = self.y + sin(direction - 0.17) * self.radius + 1
        x2 = self.x + cos(direction) * self.radius + 1
        y2 = self.y + sin(direction) * self.radius + 1
        x3 = self.x + cos(direction + 0.17) * self.radius + 1
        y3 = self.y + sin(direction + 0.17) * self.radius + 1
        pygame.draw.line(display, COLOR0, (self.x, self.y), (x1, y1), 3)
        pygame.draw.line(display, COLOR0, (self.x, self.y), (x2, y2), 3)
        pygame.draw.line(display, COLOR0, (self.x, self.y), (x3, y3), 3)

class Catcher(primitives.Arc):
    def __init__(self, x, y, inRadius, outRadius, angle0, angle1):
        primitives.Arc.__init__(self, x, y, inRadius, outRadius, angle0, angle1)
        
        self.xPrev = x
        self.yPrev = y
        
        self.fix = PADDLE_FIX
        
        self.rect = pygame.Rect(self.getRectangle())
        self.image = render.silhouette(self, COLOR3, True)
    
    def update(self):
        self.x = self.x * (1 - self.fix) + pygame.mouse.get_pos()[0] * self.fix
        self.rect.centerx = self.x
    
    def draw(self, display):
        display.blit(self.image, self.rect)

class Game:
    def __init__(self):
        #Variables
        self.avaibleBalls = STARTING_BALLS
        self.score = 0
        
        #Objects
        self.ballWidget = BallWidget(2, 2, 128, 12)
        self.ballWidget.drawImage()
        self.GUI = GUI(50, 50)
        self.GUI.score = self.score
        self.GUI.balls = self.avaibleBalls
        self.GUI.drawImage()
        
        self.level = Level()
        
        self.particleSystem = particle.System()
        
        self.emitter = Emitter(320, 24)
        self.catcher = Catcher(320, 540, 100 - BLOCK_SIZE, 100, -pi*0.75, -pi*0.25)
        
        #Containers
        self.balls = set()
        self.toKill = set()
        
        self.level.load()
    
    def reset(self):
        self.avaibleBalls = STARTING_BALLS
        self.score = 0
        
        self.GUI.score = self.score
        self.GUI.balls = self.avaibleBalls
        self.GUI.drawImage()
        
        self.level.quadTree = QuadTree(boundingRect=(0, 0, ROOM_WIDTH, ROOM_HEIGHT))
        self.particleSystem.clear()
        
        self.balls = set()
        self.toKill = set()
        
        self.level.blocks = set()
        self.level.load()
    
    def launchBall(self):
        x, y = pygame.mouse.get_pos()
        angle = getDirection(self.emitter.x, self.emitter.y, x, y)
        ball = Ball(self.emitter.x, self.emitter.y, direction=angle)
        
        self.avaibleBalls -= 1
        self.balls.add(ball)
        return ball
    
    def win(self): #Will be overridden with main module methods
        pass
    
    def lose(self): #Will be overridden with main module methods
        pass
    
    def winCondition(self):
        return len(self.level.blocks) == 0
    
    def loseCondition(self):
        return self.avaibleBalls == 0 and len(self.balls) == 0
    
    def newTurn(self):
        self.GUI.balls = self.avaibleBalls
        self.GUI.drawImage()
        self.ballWidget.drawImage(self.avaibleBalls / float(STARTING_BALLS))
        
        self.level.blocks -= self.toKill
        for block in self.toKill:
            self.particleSystem.add(particle.Explosion(block.x, block.y, BALL_SIZE*1.5))
        self.toKill.clear()
        
        #Have I won or lost or what?
        if self.winCondition():
            self.reset()
            self.win()
        elif self.loseCondition():
            self.reset()
            self.lose()
        else: #Keep gaming
            if self.level.blocks:
                self.level.quadTree = QuadTree(items=self.level.blocks)
    
    def addScore(self):
        self.score += sqrt(len(self.toKill))
        self.GUI.score = int(self.score)
        self.GUI.drawImage()
    
    def collide(self):
        for ball in self.balls:
            for block in self.level.quadTree.hit(ball.rect):
                collision, cx, cy = block.collideCircle(ball.x, ball.y, ball.radius)
                if collision:
                    #Dynamics
                    px, py = pointOnLine(ball.xPrev, ball.yPrev, ball.x, ball.y, cx, cy)
                    ball.xPrev = px * 2 - ball.xPrev
                    ball.yPrev = py * 2 - ball.yPrev
                    
                    ball.xPrev, ball.x = ball.x, ball.xPrev
                    ball.yPrev, ball.y = ball.y, ball.yPrev
                    
                    #Gameplay
                    ball.bumps += 1
                    if not block.touched:
                        block.touched = True
                        block.image = block.hitImage
                        self.toKill.add(block)
                        self.addScore()
                        self.particleSystem.add(particle.Text(px, py, str(sqrt(len(self.toKill)))[:3]))
                    self.particleSystem.add(particle.Explosion(px, py))
            
            if self.catcher.rect.colliderect(ball.rect):
                if self.catcher.collideCircle(ball.x, ball.y, ball.radius):
                    px, py = pointOnLine(ball.xPrev, ball.yPrev, ball.x, ball.y, self.catcher.x, self.catcher.y)
                    ball.xPrev = px * 2 - ball.xPrev
                    ball.yPrev = py * 2 - ball.yPrev
                    
                    ball.xPrev, ball.x = ball.x, ball.xPrev #Invert motion
                    ball.yPrev, ball.y = ball.y, ball.yPrev
                    
                    ball.accelerate(0.1)
                    
                    self.particleSystem.add(particle.Explosion(ball.x, ball.y, BALL_SIZE / 2))
    
    def update(self, events):
        #Handle events
        for event in events:
            if event.type == pygame.QUIT:
                self.lose()
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:
                    if self.avaibleBalls > 0:
                        self.launchBall()
                    if len(self.balls) > 0:
                        self.newTurn()
        
        #Handle dynamics twice for MASSIVE_PRECISION
        for i in 0, 1:
            for ball in self.balls:
                ball.update()
            self.collide()
        
        #Update game logic
        self.catcher.update()
        self.particleSystem.update()
        
        deadBalls = set()
        for ball in self.balls:
            if ball.y > ROOM_HEIGHT + ball.radius:
                deadBalls.add(ball)
            elif ball.bumps > BALL_MAX_BUMPS:
                deadBalls.add(ball)
        for ball in deadBalls:
            self.particleSystem.add(particle.Explosion(ball.x, ball.y, BALL_SIZE*1.5))
        self.balls -= deadBalls
        if deadBalls and len(self.balls) == 0:
            self.newTurn()
    
    def draw(self, display):
        display.fill(COLOR0)
        
        self.emitter.draw(display)
        self.catcher.draw(display)
        
        self.ballWidget.draw(display)
        self.GUI.draw(display)
        
        for ball in self.balls: ball.draw(display)
        self.level.draw(display)
        self.particleSystem.draw(display)